<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> 
<html>
<head>
<title>Ein Tetrisspiel in PyQt4</title>
<link rel="stylesheet" href="../format.css" type="text/css">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="keywords" content="PyQt, PyQt4, tetris game, tutorial, python, Linux">
<meta nam="description" content="Ein Tetrisspiel in PyQt4">
<meta name="language" content="de">
<meta name="author" content="ján bodnár">
<meta name="distribution" content="global">
<meta name="translation" content="manuel stein">
</head>

<body>

<div class="content">


<a href="/" title="Home">Home</a>&nbsp;
<a href=".." title="Home">Contents</a>


<h1>Ein Tetrisspiel in PyQt4</h1>

<p>
Ein Computerspiel zu schreiben ist eine große Herausforderung. Früher 
oder später will ein Programmierer ein Computerspiel entwickeln. In der 
Tat kommen viele Menschen überhaupt erst zum Programmieren, weil sie Spiele 
gespielt haben und schließlich selbst mal eins entwickeln möchten. 
Die Entwicklung eines Computerspiels wird Ihnen sehr dabei helfen, 
Ihre Programmierfertigkeiten zu verbessern.
</p>


<h3>Tetris</h3>

<p>
  Tetris ist eines der populärsten Computerspiele, das jemals geschrieben wurde.
Das Original wurde 1985 von dem russischen Programmierer <strong>Alexey Pajitnov</strong> 
entworfen und programmiert. Seit damals ist Tetris auf fast jeder Computer-Plattform und in 
etlichen Variationen verfügbar. Selbst auf meinem Mobil-Telefon befindet sich eine modifizierte 
Version von Tetris.
</p>

<p>
Tetris ist ein Falling-Block-Puzzle-Spiel, bei dem es sieben unterschiedliche Figuren 
namens <strong>Tetrominos</strong> gibt: S-Form, Z-Form, T-Form, L-Form, 
Linien-Form, Spiegel-L-Form und eine Quadrat-Form. Jede dieser Formen wird 
aus vier Quadraten zusammengesetzt. Die Formen fallen abwärts das Spielfeld herab. 
Inhalt des Tetris-Spiels ist das Drehen und Bewegen der Formen, sodass sie so gut 
wie möglich ineinander passen. Wenn es gelingt, eine Reihe zu bilden, wird diese 
zerstört und wir erhalten dafür Punkte. Das Spiel endet, wenn der obere Rand des 
Spielfelds erreicht wurde.
</p>

<img src="../images/tetrominoes.png" alt="Tetrominos" />
<p class="figure">Abbildung: Tetrominos</p>

<p>
PyQt4 ist ein Toolkit für das Erstellen von Anwendungen. Es gibt andere Bibliotheken, 
die speziell auf die Entwicklung von Computerspielen ausgerichtet sind. Nichts desto 
trotz können PyQt4 und andere Anwendungs-Toolkits für das Erstellen von Spielen 
eingesetzt werden.
</p>

<p>
Das folgende Beispiel ist eine modifizierte Version von Tetris inklusive der PyQt4-Installationsdateien.
</p>

<h3>Die Entwicklung</h3>

<p>
Da wir keine Bilder für unser Tetris haben, zeichnen wir die Tetrominos mit Hilfe der 
im PyQt-Toolkit verfügbaren Zeichen-API. Hinter jedem Computerspiel steht ein 
mathematisches Modell - so auch in Tetris.
</p>

<p>
Einige Gedanken zum Spiel.
</p>

<ul>
<li>Wir verwenden <em>QtCore.QBasicTimer()</em>, um einen Spielzyklus zu erstellen</li>
<li>Die Tetrominos werden gezeichnet</li>
<li>Die Formen bewegen sich von Quadrat zu Quadrat, nicht Pixel für Pixel.</li>
<li>Mathematisch betrachtet ist das Spielbrett lediglich eine Liste von Ziffern.</li>
</ul>

<pre class="code">
<code>
#!/usr/bin/python

# tetris.py

import sys
import random
from PyQt4 import QtCore, QtGui


class Tetris(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)

        self.setGeometry(300, 300, 180, 380)
        self.setWindowTitle('Tetris')
	self.tetrisboard = Board(self)

	self.setCentralWidget(self.tetrisboard)

	self.statusbar = self.statusBar()
	self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"), 
	    self.statusbar, QtCore.SLOT("showMessage(QString)"))

	self.tetrisboard.start()
        self.center()

    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()
        size =  self.geometry()
        self.move((screen.width()-size.width())/2, 
	    (screen.height()-size.height())/2)

class Board(QtGui.QFrame):
    BoardWidth = 10
    BoardHeight = 22
    Speed = 300

    def __init__(self, parent):
        QtGui.QFrame.__init__(self, parent)

        self.timer = QtCore.QBasicTimer()
        self.isWaitingAfterLine = False
        self.curPiece = Shape()
        self.nextPiece = Shape()
        self.curX = 0
        self.curY = 0
        self.numLinesRemoved = 0
        self.board = []

        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.isStarted = False
        self.isPaused = False
        self.clearBoard()

        self.nextPiece.setRandomShape()

    def shapeAt(self, x, y):
        return self.board[(y * Board.BoardWidth) + x]

    def setShapeAt(self, x, y, shape):
        self.board[(y * Board.BoardWidth) + x] = shape

    def squareWidth(self):
        return self.contentsRect().width() / Board.BoardWidth

    def squareHeight(self):
        return self.contentsRect().height() / Board.BoardHeight

    def start(self):
        if self.isPaused:
            return

        self.isStarted = True
        self.isWaitingAfterLine = False
        self.numLinesRemoved = 0
        self.clearBoard()

        self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), 
	    str(self.numLinesRemoved))

        self.newPiece()
        self.timer.start(Board.Speed, self)

    def pause(self):
        if not self.isStarted:
            return

        self.isPaused = not self.isPaused
        if self.isPaused:
            self.timer.stop()
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "paused")
        else:
            self.timer.start(Board.Speed, self)
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), 
	        str(self.numLinesRemoved))

        self.update()

    def paintEvent(self, event):
        painter = QtGui.QPainter(self)
        rect = self.contentsRect()

        boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()

        for i in range(Board.BoardHeight):
            for j in range(Board.BoardWidth):
                shape = self.shapeAt(j, Board.BoardHeight - i - 1)
                if shape != Tetrominoes.NoShape:
                    self.drawSquare(painter,
                        rect.left() + j * self.squareWidth(),
                        boardTop + i * self.squareHeight(), shape)

        if self.curPiece.shape() != Tetrominoes.NoShape:
            for i in range(4):
                x = self.curX + self.curPiece.x(i)
                y = self.curY - self.curPiece.y(i)
                self.drawSquare(painter, rect.left() + x * self.squareWidth(),
                    boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
                    self.curPiece.shape())

    def keyPressEvent(self, event):
        if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
            QtGui.QWidget.keyPressEvent(self, event)
            return

        key = event.key()
	if key == QtCore.Qt.Key_P:
	    self.pause()
            return
	if self.isPaused:
            return
        elif key == QtCore.Qt.Key_Left:
            self.tryMove(self.curPiece, self.curX - 1, self.curY)
        elif key == QtCore.Qt.Key_Right:
            self.tryMove(self.curPiece, self.curX + 1, self.curY)
        elif key == QtCore.Qt.Key_Down:
            self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
        elif key == QtCore.Qt.Key_Up:
            self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
        elif key == QtCore.Qt.Key_Space:
            self.dropDown()
        elif key == QtCore.Qt.Key_D:
            self.oneLineDown()
        else:
            QtGui.QWidget.keyPressEvent(self, event)

    def timerEvent(self, event):
        if event.timerId() == self.timer.timerId():
            if self.isWaitingAfterLine:
                self.isWaitingAfterLine = False
                self.newPiece()
            else:
                self.oneLineDown()
        else:
            QtGui.QFrame.timerEvent(self, event)

    def clearBoard(self):
        for i in range(Board.BoardHeight * Board.BoardWidth):
	    self.board.append(Tetrominoes.NoShape)

    def dropDown(self):
        newY = self.curY
        while newY > 0:
            if not self.tryMove(self.curPiece, self.curX, newY - 1):
                break
            newY -= 1

        self.pieceDropped()

    def oneLineDown(self):
        if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
            self.pieceDropped()

    def pieceDropped(self):
        for i in range(4):
            x = self.curX + self.curPiece.x(i)
            y = self.curY - self.curPiece.y(i)
            self.setShapeAt(x, y, self.curPiece.shape())

        self.removeFullLines()

        if not self.isWaitingAfterLine:
            self.newPiece()

    def removeFullLines(self):
        numFullLines = 0

	rowsToRemove = []

	for i in range(Board.BoardHeight):
	    n = 0
            for j in range(Board.BoardWidth):
                if not self.shapeAt(j, i) == Tetrominoes.NoShape:
                    n = n + 1

	    if n == 10:
		rowsToRemove.append(i)

	rowsToRemove.reverse()

	for m in rowsToRemove:
	    for k in range(m, Board.BoardHeight):
	        for l in range(Board.BoardWidth):
                    self.setShapeAt(l, k, self.shapeAt(l, k + 1))

        numFullLines = numFullLines + len(rowsToRemove)

        if numFullLines > 0:
            self.numLinesRemoved = self.numLinesRemoved + numFullLines
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), 
		str(self.numLinesRemoved))
            self.isWaitingAfterLine = True
            self.curPiece.setShape(Tetrominoes.NoShape)
            self.update()

    def newPiece(self):
        self.curPiece = self.nextPiece
        self.nextPiece.setRandomShape()
        self.curX = Board.BoardWidth / 2 + 1
        self.curY = Board.BoardHeight - 1 + self.curPiece.minY()

        if not self.tryMove(self.curPiece, self.curX, self.curY):
            self.curPiece.setShape(Tetrominoes.NoShape)
            self.timer.stop()
            self.isStarted = False
            self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game over")



    def tryMove(self, newPiece, newX, newY):
        for i in range(4):
            x = newX + newPiece.x(i)
            y = newY - newPiece.y(i)
            if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
                return False
            if self.shapeAt(x, y) != Tetrominoes.NoShape:
                return False

        self.curPiece = newPiece
        self.curX = newX
        self.curY = newY
        self.update()
        return True

    def drawSquare(self, painter, x, y, shape):
        colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
                      0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]

        color = QtGui.QColor(colorTable[shape])
        painter.fillRect(x + 1, y + 1, self.squareWidth() - 2, 
	    self.squareHeight() - 2, color)

        painter.setPen(color.light())
        painter.drawLine(x, y + self.squareHeight() - 1, x, y)
        painter.drawLine(x, y, x + self.squareWidth() - 1, y)

        painter.setPen(color.dark())
        painter.drawLine(x + 1, y + self.squareHeight() - 1,
            x + self.squareWidth() - 1, y + self.squareHeight() - 1)
        painter.drawLine(x + self.squareWidth() - 1, 
	    y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)


class Tetrominoes(object):
    NoShape = 0
    ZShape = 1
    SShape = 2
    LineShape = 3
    TShape = 4
    SquareShape = 5
    LShape = 6
    MirroredLShape = 7


class Shape(object):
    coordsTable = (
        ((0, 0),     (0, 0),     (0, 0),     (0, 0)),
        ((0, -1),    (0, 0),     (-1, 0),    (-1, 1)),
        ((0, -1),    (0, 0),     (1, 0),     (1, 1)),
        ((0, -1),    (0, 0),     (0, 1),     (0, 2)),
        ((-1, 0),    (0, 0),     (1, 0),     (0, 1)),
        ((0, 0),     (1, 0),     (0, 1),     (1, 1)),
        ((-1, -1),   (0, -1),    (0, 0),     (0, 1)),
        ((1, -1),    (0, -1),    (0, 0),     (0, 1))
    )

    def __init__(self):
        self.coords = [[0,0] for i in range(4)]
        self.pieceShape = Tetrominoes.NoShape

        self.setShape(Tetrominoes.NoShape)

    def shape(self):
        return self.pieceShape

    def setShape(self, shape):
        table = Shape.coordsTable[shape]
        for i in range(4):
            for j in range(2):
                self.coords[i][j] = table[i][j]

        self.pieceShape = shape

    def setRandomShape(self):
        self.setShape(random.randint(1, 7))

    def x(self, index):
        return self.coords[index][0]

    def y(self, index):
        return self.coords[index][1]

    def setX(self, index, x):
        self.coords[index][0] = x

    def setY(self, index, y):
        self.coords[index][1] = y

    def minX(self):
        m = self.coords[0][0]
        for i in range(4):
            m = min(m, self.coords[i][0])

        return m

    def maxX(self):
        m = self.coords[0][0]
        for i in range(4):
            m = max(m, self.coords[i][0])

        return m

    def minY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = min(m, self.coords[i][1])

        return m

    def maxY(self):
        m = self.coords[0][1]
        for i in range(4):
            m = max(m, self.coords[i][1])

        return m

    def rotatedLeft(self):
        if self.pieceShape == Tetrominoes.SquareShape:
            return self

        result = Shape()
        result.pieceShape = self.pieceShape
        for i in range(4):
            result.setX(i, self.y(i))
            result.setY(i, -self.x(i))

        return result

    def rotatedRight(self):
        if self.pieceShape == Tetrominoes.SquareShape:
            return self

        result = Shape()
        result.pieceShape = self.pieceShape
        for i in range(4):
            result.setX(i, -self.y(i))
            result.setY(i, self.x(i))

        return result


app = QtGui.QApplication(sys.argv)
tetris = Tetris()
tetris.show()
sys.exit(app.exec_())
</code>
</pre>

<p>
Ich habe das Spiel ein wenig vereinfacht, sodass es leichter zu verstehen ist. 
Das Spiel beginnt sofort, nachdem es gestartet wurde. Wir können das Spiel pausieren, 
indem wir die P-Taste drücken. Die Leertaste lässt die Tetrominos unmittelbar zu Boden 
fallen. Das Spiel verläuft in konstanter Geschwindigkeit, Beschleunigung wurde nicht 
eingebaut. Die Punktzahl besteht aus der Anzahl der Reihen, die wir entfernt haben.
</p>


<pre class="explanation">
<code>
 self.statusbar = self.statusBar()
 self.connect(self.tetrisboard, QtCore.SIGNAL("messageToStatusbar(QString)"), 
     self.statusbar, QtCore.SLOT("showMessage(QString)"))
</code>
</pre>

<p>
Wir erzeugen eine Statusleiste, in der wir drei mögliche Nachrichten anzeigen lassen: 
Die Zahl der bereits entfernten  Reihen, die Pause-Nachricht und die Game-Over-Nachricht.
</p>


<pre class="explanation">
<code>
 ...
 self.curX = 0
 self.curY = 0
 self.numLinesRemoved = 0
 self.board = []
 ...
</code>
</pre>

<p>
Bevor wir einen Spielzyklus starten, initialisieren wir einige wichtige Variablen. 
Die <em>self.board</em>-Variable ist eine Ziffernliste von 0 bis 7. Sie repräsentiert 
die Position verschiedener Figuren und Überbleibsel der Formen auf dem Spielbrett.
</p>

<pre class="explanation">
<code>
 for j in range(Board.BoardWidth):
     shape = self.shapeAt(j, Board.BoardHeight - i - 1)
     if shape != Tetrominoes.NoShape:
         self.drawSquare(painter,
             rect.left() + j * self.squareWidth(),
             boardTop + i * self.squareHeight(), shape)
</code>
</pre>

<p>
Die grafische Darstellung des Spiels wird in zwei Schritte unterteilt: Im ersten 
zeichnen wir die Figuren oder deren verbliebene Reste, die bereits auf den Boden 
des Spielbretts gesunken sind. Die Quadrate werden in der <em>self.board</em>-Liste 
gespeichert. Wir greifen darauf mit der <em>shapeAt()</em>-Methode zu.
</p>

<pre class="explanation">
<code>
 if self.curPiece.shape() != Tetrominoes.NoShape:
     for i in range(4):
         x = self.curX + self.curPiece.x(i)
         y = self.curY - self.curPiece.y(i)
         self.drawSquare(painter, rect.left() + x * self.squareWidth(),
             boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
             self.curPiece.shape())
</code>
</pre>

<p>
Der nächste Schritt ist das Zeichnen des im Moment herunterfallenden Stücks.
</p>


<pre class="explanation">
<code>
 elif key == QtCore.Qt.Key_Left:
     self.tryMove(self.curPiece, self.curX - 1, self.curY)
 elif key == QtCore.Qt.Key_Right:
     self.tryMove(self.curPiece, self.curX + 1, self.curY)
</code>
</pre>

<p>
Mit dem <em>keyPressEvent</em> überwachen wir das Drücken von Tasten. 
Wenn wird die rechte Pfeiltaste drücken, versuchen wir, das Teil nach 
rechts zu verschieben. <q>Versuchen</q>, weil es durchaus möglich ist, 
dass sich das Teil nicht bewegen lässt.
</p>

<pre class="explanation">
<code>
 def tryMove(self, newPiece, newX, newY):
     for i in range(4):
         x = newX + newPiece.x(i)
         y = newY - newPiece.y(i)
         if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
             return False
         if self.shapeAt(x, y) != Tetrominoes.NoShape:
             return False

     self.curPiece = newPiece
     self.curX = newX
     self.curY = newY
     self.update()
     return True
</code>
</pre>

<p>
Mit der <em>tryMove()</em>-Methode versuchen wir unsere Figuren zu bewegen. 
Wenn die Figur sich am Rand des Boards befindet oder an ein anderes Teil angrenzt, 
gibt die Methode <strong>false</strong> zurück. Andernfalls platzieren wir das 
aktuell sinkende Teil auf eine neue Position.
</p>


<pre class="explanation">
<code>
 def timerEvent(self, event):
     if event.timerId() == self.timer.timerId():
         if self.isWaitingAfterLine:
             self.isWaitingAfterLine = False
             self.newPiece()
         else:
             self.oneLineDown()
     else:
         QtGui.QFrame.timerEvent(self, event)
</code>
</pre>

<p>
Mit dem Timer-Ereignis erzeugen wir ein neues Teil, nachdem das vorangegangene 
den Boden erreicht hat oder wir bewegen ein sinkendes Teil eine Zeile tiefer.
</p>

<pre class="explanation">
<code>
 def removeFullLines(self):
     numFullLines = 0

     rowsToRemove = []

     for i in range(Board.BoardHeight):
         n = 0
         for j in range(Board.BoardWidth):
             if not self.shapeAt(j, i) == Tetrominoes.NoShape:
                 n = n + 1

         if n == 10:
             rowsToRemove.append(i)

      rowsToRemove.reverse()

      for m in rowsToRemove:
          for k in range(m, Board.BoardHeight):
              for l in range(Board.BoardWidth):
                  self.setShapeAt(l, k, self.shapeAt(l, k + 1))
 ...
</code>
</pre>

<p>
Wenn das Teil den Boden erreicht, rufen wir die <em>removeFullLines()</em>-Methode auf. 
Zunächst finden wir alle vollständigen Zeilen heraus und entfernen sie, indem wir alle 
Zeilen über der im Moment vollen, zu entfernenden eine Zeile herab. Beachten Sie, dass 
wir die Reihenfolge der zu entfernenden Zeilen umdrehen, andernfalls würde es nicht richtig funktionieren.
In unserem Fall verwenden wir <em>naive Schwerkraft</em>, d.h., dass die Teile über leere Lücken gleiten.
</p>

<pre class="explanation">
<code>
 def newPiece(self):
     self.curPiece = self.nextPiece
     self.nextPiece.setRandomShape()
     self.curX = Board.BoardWidth / 2 + 1
     self.curY = Board.BoardHeight - 1 + self.curPiece.minY()

     if not self.tryMove(self.curPiece, self.curX, self.curY):
         self.curPiece.setShape(Tetrominoes.NoShape)
         self.timer.stop()
         self.isStarted = False
         self.emit(QtCore.SIGNAL("messageToStatusbar(QString)"), "Game over")
</code>
</pre>

<p>
Die <em>newPiece()</em>-Methode erzeugt zufällig ein neues Tetris-Stück. Wenn das 
Stück nicht in seine anfängliche Position kann, ist das Spiel vorbei.
</p>

<p>
Die <em>Shape</em>-Klasse speichert Informationen über das Tetris-Stück.
</p>


<pre class="explanation">
<code>
 self.coords = [[0,0] for i in range(4)]
</code>
</pre>

<p>
Bei der Erstellung erzeugen wir eine leere Koordinatenliste. Die Liste wird die Koordinaten 
der Tetris-Stücke speichern. Zum Beispiel repräsentieren die Tupel (0,-1), (0,0), (0,1), (1,1)
eine rotierte S-Form. Das folgende Diagramm illustriert ihr Aussehen.
</p>

<img src="../images/coordinates.png" alt="Koordinaten" />
<p class="figure">Abbildung: Koordinaten</p>

<p>
Wenn wir das aktuell sinkende Stück zeichnen, zeichnen wir es an der Position <em>self.curX></em> und 
<em>self.curY</em>. Dann werfen wir einen Blick auf die Koordinaten  in der Tabelle und zeichnen alle vier Quadrate.
</p>


<img src="../images/tetris.png" alt="Tetris">
<div class="figure">Abbildung: Tetris</div>


<div class="footer">
<div class="signature">
<a href="/">ZetCode</a> last modified November 24, 2007  <span class="copyright">&copy; 2007 - 2011 Jan Bodnar</span>
<span class="copyright">Translation © 2009 Manuel Stein</span>
</div>
</div>

</div> <!-- content -->
</body>
</html>

